Skip to content

Honor subtyping in generic class match arms' reified type-arg check (B-634) - #3902

Merged
antoniosarosi merged 2 commits into
canaryfrom
antonio/b-634
Jul 2, 2026
Merged

antoniosarosi merged 2 commits into
canaryfrom
antonio/b-634

Conversation

@antoniosarosi

@antoniosarosi antoniosarosi commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

Bug

A generic match arm let s: Opt<T> (where T is the enclosing function's type var) lowered its runtime type-test to an exact reified type-arg comparison. When inference pins T to a supertype union of the scrutinee's actual type arg — which happens whenever a default: T argument subtypes, so T reifies to the un-subsumed join Shape | Sq while the value is a plain Opt<Shape> — the exact Shape | Sq == Shape check fails, the arm silently misses, and control falls through to the default.

Repro

interface Shape { function area(self) -> int }
class Sq { s: int, implements Shape { function area(self) -> int { self.s * self.s } } }
class Opt<T> { value: T }
class Non {}

function mk<T>(v: T) -> Opt<T> | Non { Opt<T> { value: v } }

function unwrap_or<T>(o: Opt<T> | Non, default: T) -> T {
  match (o) {
    let s: Opt<T> => s.value,
    _ => default,
  }
}

function bug() -> int {
  let o: Opt<Shape> | Non = mk<Shape>(Sq { s: 5 });
  unwrap_or(o, Sq { s: 0 }).area()   // T infers to `Shape | Sq`
}

bug() returned 0 (expected 25). Widening the default so T pins to Shape directly (let d: Shape = Sq{s:0}) already returned 25. Probing reflect.type_of<T>() inside unwrap_or prints "Shape | Sq", confirming the reified frame type-arg is the un-subsumed join, not Sq.

Root cause

crates/baml_compiler2_mir/src/lower.rs, lower_pattern_test: the typevar-carrying class-pattern branch built the IsType template via ty_to_template, whose TypeVar arm lowers a frame type var to TyTemplate::TypeArgRef(N) (exact). The VM's guard_template_matches (crates/bex_vm/src/vm.rs) checks TypeArgRef(n) with frame_type_args[n] == actual — structural equality. With frame_type_args[0] = Union([Shape, Sq]) and the scrutinee Opt<Shape>'s class_type_args[0] = Shape, Union([Shape, Sq]) == Shape is false, so the arm never matches.

The sibling TypeArgRefOrWildcard(n) arm instead does actual.is_subtype_of(expected) — the correct covariant "does this value belong to type Opt<T>" semantics — and this is exactly what interface class-dispatch guards already use (emit_interface_class_guard_branch, via tir2_to_dispatch_guard_template). Match arms were inconsistently on the exact path.

Fix (MIR layer)

Swap the single ty_to_template call at the typevar class-pattern arm for tir2_to_dispatch_guard_template, so Opt<T> lowers to Class("Opt", [TypeArgRefOrWildcard(0)]). The VM then subtype-checks Shape <: (Shape | Sq) → matches → returns s.value → area() == 25.

This is preferred over broadening the VM's TypeArgRef comparison to is_subtype_of, because it keeps TypeArgRef's exact-materialization meaning intact and makes match/catch class-pattern arms consistent with the already-shipping interface-class-dispatch guard.

Directionality / soundness

The subtype check stays directional: a strictly wider runtime type-arg still fails to match a narrower pinned T ((Shape|Sq).is_subtype_of(Sq) is false). Static typing + covariance already guarantee a well-typed scrutinee's runtime arg is <: the frame T, so this cannot over-match well-typed programs. Concrete parametric arms (Foo<int> vs Foo<string>) carry no enclosing type var and take the untouched exact concrete path.

Tests

  • bex_vm unit tests on guard_template_matches locking the directional subtype semantics: under-match fix (Shape matches a Shape|Sq frame T), exact TypeArgRef stays exact, a wider runtime arg does not over-match a narrower T, an unknown frame slot is a wildcard, and concrete Foo<int> vs Foo<string> still discriminate.
  • ns_generic_match_subtype e2e (real VM): narrow-default → 25, widened-default → 25 (control), non-Opt scrutinee falls through to the default, and concrete Foo<int>/Foo<string> still discriminate.
  • projects/compiles/generic_match_typevar_arm snapshot project locks the emitted template as TypeArgRefOrWildcard (Opt<#0?>, was Opt<#0>) in the MIR/codegen snapshots.

Before / after

function before after
bug2_narrow (narrow default) 0 25
bug2_widened (widened default, control) 25 25

Snapshots

No existing e2e/compiled snapshot shifted (verified across the whole baml_src bytecode snapshot: only the new namespace snapshot and the global $init_test registrations appear). New snapshots: the generic_match_typevar_arm compiles project and the generic_match_subtype namespace. The _root.snap change is just the added test registrations.

Local runs green: cargo nextest run -p bex_vm -p baml_compiler2_mir (68 passed) and cargo test -p baml_tests -- --skip parser_stress (2758 passed, 0 failed).

https://linear.app/boundaryml2/issue/B-634

Summary by CodeRabbit

  • Bug Fixes

    • Improved generic match pattern runtime type checks so subtype-based arms continue to match correctly when type inference widens generic arguments.
  • Tests

    • Added end-to-end scenarios for widened generic types, correct arm selection, non-matching fallthrough, and concrete generic discrimination.
    • Added unit tests to verify VM dispatch-guard behavior for subtype-or-wildcard vs exact type-template matching.

…B-634)

A generic `match` arm `let s: Opt<T>` (where `T` is the enclosing function's
type var) lowered its runtime type-test to an EXACT reified type-arg comparison.
When inference pins `T` to a supertype union of the scrutinee's actual type arg
-- which happens whenever a `default: T` argument subtypes, so `T` reifies to
the un-subsumed join `Shape | Sq` while the value is a plain `Opt<Shape>` -- the
exact `Shape | Sq == Shape` check failed and the arm silently missed, falling
through to the default. Concretely, `unwrap_or(mk<Shape>(Sq{s:5}), Sq{s:0})
.area()` returned 0 instead of 25; widening the default to `Shape` (so `T`
pins to `Shape`) already returned 25.

Fix (MIR layer): in `lower_pattern_test`, the typevar-carrying class-pattern arm
now builds its `IsType` template with `tir2_to_dispatch_guard_template` (frame
`TypeVar` -> `TypeArgRefOrWildcard`, subtype-or-wildcard) instead of
`ty_to_template` (frame `TypeVar` -> `TypeArgRef`, exact). The VM then compares
the reified frame arg with `is_subtype_of` rather than `==`, which is the
correct covariant "does this value belong to type `Opt<T>`" semantics and
matches the interface class-dispatch guard path (`emit_interface_class_guard_
branch`), which already uses the same template. This keeps `TypeArgRef`'s
exact-materialization meaning intact for other uses.

Directionality is preserved: a strictly wider runtime type-arg still fails to
match a narrower pinned `T`. Concrete parametric arms (`Foo<int>` vs
`Foo<string>`) are unaffected -- they carry no enclosing type var and take the
exact concrete path.

Tests:
- bex_vm unit tests on `guard_template_matches` locking the directional
  subtype semantics (under-match fix, exact `TypeArgRef` stays exact, wider
  arg does not over-match, unknown-slot wildcard, concrete discrimination).
- ns_generic_match_subtype e2e: narrow-default -> 25, widened-default -> 25,
  non-Opt scrutinee falls through, concrete `Foo<int>`/`Foo<string>` still
  discriminate.
- projects/compiles/generic_match_typevar_arm locks the emitted template as
  `TypeArgRefOrWildcard` (`Opt<#0?>`) in the MIR/codegen snapshots.
@linear

linear Bot commented Jul 1, 2026

Copy link
Copy Markdown

B-634

@vercel

vercel Bot commented Jul 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Jul 1, 2026 11:51pm
promptfiddle Ready Ready Preview, Comment Jul 1, 2026 11:51pm
promptfiddle2 Ready Ready Preview, Comment Jul 1, 2026 11:51pm

Request Review

@vercel
vercel Bot temporarily deployed to Preview – beps July 1, 2026 23:33 Inactive
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b0190197-c79c-499e-a6ec-09a219783e2e

📥 Commits

Reviewing files that changed from the base of the PR and between b82b418 and 57cc6c1.

📒 Files selected for processing (1)
  • baml_language/crates/bex_vm/src/vm.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • baml_language/crates/bex_vm/src/vm.rs

📝 Walkthrough

Walkthrough

Changes the MIR lowering of generic Class(..) pattern tests to use dispatch-guard templates when type variables remain, and adds VM and BAML regression coverage for subtype-or-wildcard matching and concrete type discrimination.

Changes

Generic Match Subtype Dispatch Guard

Layer / File(s) Summary
Dispatch-guard template for generic type-var pattern tests
baml_language/crates/baml_compiler2_mir/src/lower.rs
lower_pattern_test now uses tir2_to_dispatch_guard_template (TypeArgRefOrWildcard) instead of ty_to_template for AstPattern::Type cases where the inferred type is a generic Class(..) containing type variables.
VM-level template matching tests
baml_language/crates/bex_vm/src/vm.rs
New tests verify TypeArgRefOrWildcard matches widened frame type-args correctly, while exact TypeArgRef and concrete class type-arg templates continue to discriminate strictly.
End-to-end BAML regression fixtures
baml_language/crates/baml_tests/baml_src/ns_generic_match_subtype/generic_match_subtype.baml, baml_language/crates/baml_tests/projects/compiles/generic_match_typevar_arm/generic_match_typevar_arm.baml
New BAML test files define Shape/Sq/Opt<T>/Foo<T> types, match-based scenarios, and a compile test asserting IsType constants render TypeArgRefOrWildcard (#0?) instead of TypeArgRef (#0).

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • BoundaryML/baml#3638: Both PRs adjust generic type-test lowering and dispatch-guard matching so widened type arguments are handled by subtype-or-wildcard semantics.

Poem

A rabbit hopped through typey dust,
Where wildcards matched just as they must.
T grew wide, but still stayed true,
And Sq with Shape both got their due.
Hooray for guards that leap and flow —
Quick ears, keen eyes, and tests that ցույց?

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main fix: subtype-aware reified type-arg checks for generic class match arms.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch antonio/b-634

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
baml_language/crates/baml_compiler2_mir/src/lower.rs (1)

15188-15221: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Generic class unions still bypass the template path

baml_language/crates/baml_compiler2_mir/src/lower.rs:15188-15221 only handles top-level Tir2Ty::Class(..). Union-typed patterns like Opt<T> | Other<T> still fall through to the erased runtime lowering, and tir_ty_needs_interface_shape_test only covers Interface members. If those patterns are valid here, they need the same dispatch-guard template path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_compiler2_mir/src/lower.rs` around lines 15188 -
15221, The generic class-pattern lowering only routes top-level Tir2Ty::Class
patterns with typevars through the dispatch-guard template path, so union
patterns like Opt<T> | Other<T> still fall back to erased runtime lowering.
Update the lowering logic in lower.rs around the class-pattern branch to detect
union-typed patterns containing class members with typevars and send them
through tir2_to_dispatch_guard_template and emit_is_type_template_branch as
well, similar to the existing Class(..) handling and the interface guard path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@baml_language/crates/baml_compiler2_mir/src/lower.rs`:
- Around line 15188-15221: The generic class-pattern lowering only routes
top-level Tir2Ty::Class patterns with typevars through the dispatch-guard
template path, so union patterns like Opt<T> | Other<T> still fall back to
erased runtime lowering. Update the lowering logic in lower.rs around the
class-pattern branch to detect union-typed patterns containing class members
with typevars and send them through tir2_to_dispatch_guard_template and
emit_is_type_template_branch as well, similar to the existing Class(..) handling
and the interface guard path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 27dde455-9e06-4646-b717-3d1ea6d08df8

📥 Commits

Reviewing files that changed from the base of the PR and between cfafa66 and b82b418.

⛔ Files ignored due to path filters (10)
  • baml_language/crates/baml_tests/snapshots/baml_src/_root.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/baml_src/generic_match_subtype.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/generic_match_typevar_arm/baml_tests__compiles__generic_match_typevar_arm__01_lexer__generic_match_typevar_arm.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/generic_match_typevar_arm/baml_tests__compiles__generic_match_typevar_arm__02_parser__generic_match_typevar_arm.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/generic_match_typevar_arm/baml_tests__compiles__generic_match_typevar_arm__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/generic_match_typevar_arm/baml_tests__compiles__generic_match_typevar_arm__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/generic_match_typevar_arm/baml_tests__compiles__generic_match_typevar_arm__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/generic_match_typevar_arm/baml_tests__compiles__generic_match_typevar_arm__05_diagnostics.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/generic_match_typevar_arm/baml_tests__compiles__generic_match_typevar_arm__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/generic_match_typevar_arm/baml_tests__compiles__generic_match_typevar_arm__10_formatter__generic_match_typevar_arm.snap is excluded by !**/*.snap
📒 Files selected for processing (4)
  • baml_language/crates/baml_compiler2_mir/src/lower.rs
  • baml_language/crates/baml_tests/baml_src/ns_generic_match_subtype/generic_match_subtype.baml
  • baml_language/crates/baml_tests/projects/compiles/generic_match_typevar_arm/generic_match_typevar_arm.baml
  • baml_language/crates/bex_vm/src/vm.rs

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 1, 2026 23:39 Inactive
@github-actions

github-actions Bot commented Jul 1, 2026 •

Copy link
Copy Markdown

Binary size checks passed

✅ 7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
✅ baml-cli Linux 🔒 21.6 MB 9.2 MB file 21.5 MB +116.7 KB (+0.5%) OK
✅ packed-program Linux 🔒 15.8 MB 6.6 MB file 15.6 MB +148.6 KB (+1.0%) OK
✅ baml-cli macOS 🔒 16.6 MB 8.0 MB file 16.6 MB +66.3 KB (+0.4%) OK
✅ packed-program macOS 🔒 12.2 MB 5.8 MB file 12.1 MB +83.1 KB (+0.7%) OK
✅ baml-cli Windows 🔒 18.2 MB 8.2 MB file 18.1 MB +107.0 KB (+0.6%) OK
✅ packed-program Windows 🔒 13.1 MB 5.9 MB file 13.0 MB +129.1 KB (+1.0%) OK
✅ bridge_wasm WASM 14.5 MB 🔒 4.1 MB gzip 4.1 MB +44.7 KB (+1.1%) OK

🔒 = the size this artifact is GATED on (ceiling + delta). Binaries gate on file size (installed binary); WASM gates on gzip (download size). The other size is shown for information only.


Generated by cargo size-gate · workflow run

@vercel
vercel Bot temporarily deployed to Preview – beps July 1, 2026 23:44 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle July 1, 2026 23:44 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 1, 2026 23:51 Inactive
@antoniosarosi
antoniosarosi added this pull request to the merge queue Jul 2, 2026
Merged via the queue into canary with commit e42faa1 Jul 2, 2026
52 of 53 checks passed
@antoniosarosi
antoniosarosi deleted the antonio/b-634 branch July 2, 2026 00:26

This branch was previously deployed

3 inactive deployments
Preview – promptfiddle2 — 57cc6c13 Deployed Jul 1, 2026 by vercel[bot]
Preview – promptfiddle — 57cc6c13 Deployed Jul 1, 2026 by vercel[bot]
Preview – beps — 57cc6c13 Deployed Jul 1, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant